// "Therefore those skilled at the unorthodox
// are infinite as heaven and earth,
// inexhaustible as the great rivers.
// When they come to an end,
// they begin again,
// like the days and months;
// they die and are reborn,
// like the four seasons."
//
// - Sun Tsu,
// "The Art of War"

package cn.appoa.afimage.cropper;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.RectF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ProgressBar;

import java.lang.ref.WeakReference;
import java.util.UUID;

import androidx.exifinterface.media.ExifInterface;
import cn.appoa.afimage.R;

/**
 * Custom view that provides cropping capabilities to an image.
 */
public class CropImageView extends FrameLayout {

    // region: Fields and Consts

    /**
     * Image view widget used to show the image for cropping.
     */
    private final ImageView mImageView;

    /**
     * Overlay over the image view to show cropping UI.
     */
    private final CropOverlayView mCropOverlayView;

    /**
     * The matrix used to transform the cropping image in the image view
     */
    private final Matrix mImageMatrix = new Matrix();

    /**
     * Reusing matrix instance for reverse matrix calculations.
     */
    private final Matrix mImageInverseMatrix = new Matrix();

    /**
     * Progress bar widget to show progress bar on async image loading and cropping.
     */
    private final ProgressBar mProgressBar;

    /**
     * Rectangle used in image matrix transformation calculation (reusing rect instance)
     */
    private final float[] mImagePoints = new float[8];

    /**
     * Rectangle used in image matrix transformation for scale calculation (reusing rect instance)
     */
    private final float[] mScaleImagePoints = new float[8];

    /**
     * Animation class to smooth animate zoom-in/out
     */
    private CropImageAnimation mAnimation;

    private Bitmap mBitmap;

    /**
     * The image rotation value used during loading of the image so we can reset to it
     */
    private int mInitialDegreesRotated;

    /**
     * How much the image is rotated from original clockwise
     */
    private int mDegreesRotated;

    /**
     * if the image flipped horizontally
     */
    private boolean mFlipHorizontally;

    /**
     * if the image flipped vertically
     */
    private boolean mFlipVertically;

    private int mLayoutWidth;

    private int mLayoutHeight;

    private int mImageResource;

    /**
     * The initial scale type of the image in the crop image view
     */
    private ScaleType mScaleType;

    /**
     * if to save bitmap on save instance state.<br>
     * It is best to avoid it by using URI in setting image for cropping.<br>
     * If false the bitmap is not saved and if restore is required to view will be empty, storing the
     * bitmap requires saving it to file which can be expensive. default: false.
     */
    private boolean mSaveBitmapToInstanceState = false;

    /**
     * if to show crop overlay UI what contains the crop window UI surrounded by background over the
     * cropping image.<br>
     * default: true, may disable for animation or frame transition.
     */
    private boolean mShowCropOverlay = true;

    /**
     * if to show progress bar when image async loading/cropping is in progress.<br>
     * default: true, disable to provide custom progress bar UI.
     */
    private boolean mShowProgressBar = true;

    /**
     * if auto-zoom functionality is enabled.<br>
     * default: true.
     */
    private boolean mAutoZoomEnabled = true;

    /**
     * The max zoom allowed during cropping
     */
    private int mMaxZoom;

    /**
     * callback to be invoked when crop overlay is released.
     */
    private OnSetCropOverlayReleasedListener mOnCropOverlayReleasedListener;

    /**
     * callback to be invoked when crop overlay is moved.
     */
    private OnSetCropOverlayMovedListener mOnSetCropOverlayMovedListener;

    /**
     * callback to be invoked when crop window is changed.
     */
    private OnSetCropWindowChangeListener mOnSetCropWindowChangeListener;

    /**
     * callback to be invoked when image async loading is complete.
     */
    private OnSetImageUriCompleteListener mOnSetImageUriCompleteListener;

    /**
     * callback to be invoked when image async cropping is complete.
     */
    private OnCropImageCompleteListener mOnCropImageCompleteListener;

    /**
     * The URI that the image was loaded from (if loaded from URI)
     */
    private Uri mLoadedImageUri;

    /**
     * The sample size the image was loaded by if was loaded by URI
     */
    private int mLoadedSampleSize = 1;

    /**
     * The current zoom level to to scale the cropping image
     */
    private float mZoom = 1;

    /**
     * The X offset that the cropping image was translated after zooming
     */
    private float mZoomOffsetX;

    /**
     * The Y offset that the cropping image was translated after zooming
     */
    private float mZoomOffsetY;

    /**
     * Used to restore the cropping windows rectangle after state restore
     */
    private RectF mRestoreCropWindowRect;

    /**
     * Used to restore image rotation after state restore
     */
    private int mRestoreDegreesRotated;

    /**
     * Used to detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean,
     * boolean)} in {@link #layout(int, int, int, int)}.
     */
    private boolean mSizeChanged;

    /**
     * Temp URI used to save bitmap image to disk to preserve for instance state in case cropped was
     * set with bitmap
     */
    private Uri mSaveInstanceStateBitmapUri;

    /**
     * Task used to load bitmap async from UI thread
     */
    private WeakReference<BitmapLoadingWorkerTask> mBitmapLoadingWorkerTask;

    /**
     * Task used to crop bitmap async from UI thread
     */
    private WeakReference<BitmapCroppingWorkerTask> mBitmapCroppingWorkerTask;
    // endregion

    public CropImageView(Context context) {
        this(context, null);
    }

    public CropImageView(Context context, AttributeSet attrs) {
        super(context, attrs);

        CropImageOptions options = null;
        Intent intent = context instanceof Activity ? ((Activity) context).getIntent() : null;
        if (intent != null) {
            Bundle bundle = intent.getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
            if (bundle != null) {
                options = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
            }
        }

        if (options == null) {

            options = new CropImageOptions();

            if (attrs != null) {
                TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CropImageView, 0, 0);
                try {
                    options.fixAspectRatio =
                            ta.getBoolean(R.styleable.CropImageView_cropFixAspectRatio, options.fixAspectRatio);
                    options.aspectRatioX =
                            ta.getInteger(R.styleable.CropImageView_cropAspectRatioX, options.aspectRatioX);
                    options.aspectRatioY =
                            ta.getInteger(R.styleable.CropImageView_cropAspectRatioY, options.aspectRatioY);
                    options.scaleType =
                            ScaleType.values()[
                                    ta.getInt(R.styleable.CropImageView_cropScaleType, options.scaleType.ordinal())];
                    options.autoZoomEnabled =
                            ta.getBoolean(R.styleable.CropImageView_cropAutoZoomEnabled, options.autoZoomEnabled);
                    options.multiTouchEnabled =
                            ta.getBoolean(
                                    R.styleable.CropImageView_cropMultiTouchEnabled, options.multiTouchEnabled);
                    options.maxZoom = ta.getInteger(R.styleable.CropImageView_cropMaxZoom, options.maxZoom);
                    options.cropShape =
                            CropShape.values()[
                                    ta.getInt(R.styleable.CropImageView_cropShape, options.cropShape.ordinal())];
                    options.guidelines =
                            Guidelines.values()[
                                    ta.getInt(
                                            R.styleable.CropImageView_cropGuidelines, options.guidelines.ordinal())];
                    options.snapRadius =
                            ta.getDimension(R.styleable.CropImageView_cropSnapRadius, options.snapRadius);
                    options.touchRadius =
                            ta.getDimension(R.styleable.CropImageView_cropTouchRadius, options.touchRadius);
                    options.initialCropWindowPaddingRatio =
                            ta.getFloat(
                                    R.styleable.CropImageView_cropInitialCropWindowPaddingRatio,
                                    options.initialCropWindowPaddingRatio);
                    options.borderLineThickness =
                            ta.getDimension(
                                    R.styleable.CropImageView_cropBorderLineThickness, options.borderLineThickness);
                    options.borderLineColor =
                            ta.getInteger(R.styleable.CropImageView_cropBorderLineColor, options.borderLineColor);
                    options.borderCornerThickness =
                            ta.getDimension(
                                    R.styleable.CropImageView_cropBorderCornerThickness,
                                    options.borderCornerThickness);
                    options.borderCornerOffset =
                            ta.getDimension(
                                    R.styleable.CropImageView_cropBorderCornerOffset, options.borderCornerOffset);
                    options.borderCornerLength =
                            ta.getDimension(
                                    R.styleable.CropImageView_cropBorderCornerLength, options.borderCornerLength);
                    options.borderCornerColor =
                            ta.getInteger(
                                    R.styleable.CropImageView_cropBorderCornerColor, options.borderCornerColor);
                    options.guidelinesThickness =
                            ta.getDimension(
                                    R.styleable.CropImageView_cropGuidelinesThickness, options.guidelinesThickness);
                    options.guidelinesColor =
                            ta.getInteger(R.styleable.CropImageView_cropGuidelinesColor, options.guidelinesColor);
                    options.backgroundColor =
                            ta.getInteger(R.styleable.CropImageView_cropBackgroundColor, options.backgroundColor);
                    options.showCropOverlay =
                            ta.getBoolean(R.styleable.CropImageView_cropShowCropOverlay, mShowCropOverlay);
                    options.showProgressBar =
                            ta.getBoolean(R.styleable.CropImageView_cropShowProgressBar, mShowProgressBar);
                    options.borderCornerThickness =
                            ta.getDimension(
                                    R.styleable.CropImageView_cropBorderCornerThickness,
                                    options.borderCornerThickness);
                    options.minCropWindowWidth =
                            (int)
                                    ta.getDimension(
                                            R.styleable.CropImageView_cropMinCropWindowWidth, options.minCropWindowWidth);
                    options.minCropWindowHeight =
                            (int)
                                    ta.getDimension(
                                            R.styleable.CropImageView_cropMinCropWindowHeight,
                                            options.minCropWindowHeight);
                    options.minCropResultWidth =
                            (int)
                                    ta.getFloat(
                                            R.styleable.CropImageView_cropMinCropResultWidthPX,
                                            options.minCropResultWidth);
                    options.minCropResultHeight =
                            (int)
                                    ta.getFloat(
                                            R.styleable.CropImageView_cropMinCropResultHeightPX,
                                            options.minCropResultHeight);
                    options.maxCropResultWidth =
                            (int)
                                    ta.getFloat(
                                            R.styleable.CropImageView_cropMaxCropResultWidthPX,
                                            options.maxCropResultWidth);
                    options.maxCropResultHeight =
                            (int)
                                    ta.getFloat(
                                            R.styleable.CropImageView_cropMaxCropResultHeightPX,
                                            options.maxCropResultHeight);
                    options.flipHorizontally =
                            ta.getBoolean(
                                    R.styleable.CropImageView_cropFlipHorizontally, options.flipHorizontally);
                    options.flipVertically =
                            ta.getBoolean(R.styleable.CropImageView_cropFlipHorizontally, options.flipVertically);

                    mSaveBitmapToInstanceState =
                            ta.getBoolean(
                                    R.styleable.CropImageView_cropSaveBitmapToInstanceState,
                                    mSaveBitmapToInstanceState);

                    // if aspect ratio is set then set fixed to true
                    if (ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
                            && ta.hasValue(R.styleable.CropImageView_cropAspectRatioX)
                            && !ta.hasValue(R.styleable.CropImageView_cropFixAspectRatio)) {
                        options.fixAspectRatio = true;
                    }
                } finally {
                    ta.recycle();
                }
            }
        }

        options.validate();

        mScaleType = options.scaleType;
        mAutoZoomEnabled = options.autoZoomEnabled;
        mMaxZoom = options.maxZoom;
        mShowCropOverlay = options.showCropOverlay;
        mShowProgressBar = options.showProgressBar;
        mFlipHorizontally = options.flipHorizontally;
        mFlipVertically = options.flipVertically;

        LayoutInflater inflater = LayoutInflater.from(context);
        View v = inflater.inflate(R.layout.crop_image_view, this, true);

        mImageView = (ImageView) v.findViewById(R.id.ImageView_image);
        mImageView.setScaleType(ImageView.ScaleType.MATRIX);

        mCropOverlayView = (CropOverlayView) v.findViewById(R.id.CropOverlayView);
        mCropOverlayView.setCropWindowChangeListener(
                new CropOverlayView.CropWindowChangeListener() {
                    @Override
                    public void onCropWindowChanged(boolean inProgress) {
                        handleCropWindowChanged(inProgress, true);
                        OnSetCropOverlayReleasedListener listener = mOnCropOverlayReleasedListener;
                        if (listener != null && !inProgress) {
                            listener.onCropOverlayReleased(getCropRect());
                        }
                        OnSetCropOverlayMovedListener movedListener = mOnSetCropOverlayMovedListener;
                        if (movedListener != null && inProgress) {
                            movedListener.onCropOverlayMoved(getCropRect());
                        }
                    }
                });
        mCropOverlayView.setInitialAttributeValues(options);

        mProgressBar = (ProgressBar) v.findViewById(R.id.CropProgressBar);
        setProgressBarVisibility();
    }

    /**
     * Get the scale type of the image in the crop view.
     */
    public ScaleType getScaleType() {
        return mScaleType;
    }

    /**
     * Set the scale type of the image in the crop view
     */
    public void setScaleType(ScaleType scaleType) {
        if (scaleType != mScaleType) {
            mScaleType = scaleType;
            mZoom = 1;
            mZoomOffsetX = mZoomOffsetY = 0;
            mCropOverlayView.resetCropOverlayView();
            requestLayout();
        }
    }

    /**
     * The shape of the cropping area - rectangle/circular.
     */
    public CropShape getCropShape() {
        return mCropOverlayView.getCropShape();
    }

    /**
     * The shape of the cropping area - rectangle/circular.<br>
     * To set square/circle crop shape set aspect ratio to 1:1.
     */
    public void setCropShape(CropShape cropShape) {
        mCropOverlayView.setCropShape(cropShape);
    }

    /**
     * if auto-zoom functionality is enabled. default: true.
     */
    public boolean isAutoZoomEnabled() {
        return mAutoZoomEnabled;
    }

    /**
     * Set auto-zoom functionality to enabled/disabled.
     */
    public void setAutoZoomEnabled(boolean autoZoomEnabled) {
        if (mAutoZoomEnabled != autoZoomEnabled) {
            mAutoZoomEnabled = autoZoomEnabled;
            handleCropWindowChanged(false, false);
            mCropOverlayView.invalidate();
        }
    }

    /**
     * Set multi touch functionality to enabled/disabled.
     */
    public void setMultiTouchEnabled(boolean multiTouchEnabled) {
        if (mCropOverlayView.setMultiTouchEnabled(multiTouchEnabled)) {
            handleCropWindowChanged(false, false);
            mCropOverlayView.invalidate();
        }
    }

    /**
     * The max zoom allowed during cropping.
     */
    public int getMaxZoom() {
        return mMaxZoom;
    }

    /**
     * The max zoom allowed during cropping.
     */
    public void setMaxZoom(int maxZoom) {
        if (mMaxZoom != maxZoom && maxZoom > 0) {
            mMaxZoom = maxZoom;
            handleCropWindowChanged(false, false);
            mCropOverlayView.invalidate();
        }
    }

    /**
     * the min size the resulting cropping image is allowed to be, affects the cropping window limits
     * (in pixels).<br>
     */
    public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
        mCropOverlayView.setMinCropResultSize(minCropResultWidth, minCropResultHeight);
    }

    /**
     * the max size the resulting cropping image is allowed to be, affects the cropping window limits
     * (in pixels).<br>
     */
    public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
        mCropOverlayView.setMaxCropResultSize(maxCropResultWidth, maxCropResultHeight);
    }

    /**
     * Get the amount of degrees the cropping image is rotated cloackwise.<br>
     *
     * @return 0-360
     */
    public int getRotatedDegrees() {
        return mDegreesRotated;
    }

    /**
     * Set the amount of degrees the cropping image is rotated cloackwise.<br>
     *
     * @param degrees 0-360
     */
    public void setRotatedDegrees(int degrees) {
        if (mDegreesRotated != degrees) {
            rotateImage(degrees - mDegreesRotated);
        }
    }

    /**
     * whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows it to
     * be changed.
     */
    public boolean isFixAspectRatio() {
        return mCropOverlayView.isFixAspectRatio();
    }

    /**
     * Sets whether the aspect ratio is fixed or not; true fixes the aspect ratio, while false allows
     * it to be changed.
     */
    public void setFixedAspectRatio(boolean fixAspectRatio) {
        mCropOverlayView.setFixedAspectRatio(fixAspectRatio);
    }

    /**
     * whether the image should be flipped horizontally
     */
    public boolean isFlippedHorizontally() {
        return mFlipHorizontally;
    }

    /**
     * Sets whether the image should be flipped horizontally
     */
    public void setFlippedHorizontally(boolean flipHorizontally) {
        if (mFlipHorizontally != flipHorizontally) {
            mFlipHorizontally = flipHorizontally;
            applyImageMatrix(getWidth(), getHeight(), true, false);
        }
    }

    /**
     * whether the image should be flipped vertically
     */
    public boolean isFlippedVertically() {
        return mFlipVertically;
    }

    /**
     * Sets whether the image should be flipped vertically
     */
    public void setFlippedVertically(boolean flipVertically) {
        if (mFlipVertically != flipVertically) {
            mFlipVertically = flipVertically;
            applyImageMatrix(getWidth(), getHeight(), true, false);
        }
    }

    /**
     * Get the current guidelines option set.
     */
    public Guidelines getGuidelines() {
        return mCropOverlayView.getGuidelines();
    }

    /**
     * Sets the guidelines for the CropOverlayView to be either on, off, or to show when resizing the
     * application.
     */
    public void setGuidelines(Guidelines guidelines) {
        mCropOverlayView.setGuidelines(guidelines);
    }

    /**
     * both the X and Y values of the aspectRatio.
     */
    public Pair<Integer, Integer> getAspectRatio() {
        return new Pair<Integer, Integer>(mCropOverlayView.getAspectRatioX(), mCropOverlayView.getAspectRatioY());
    }

    /**
     * Sets the both the X and Y values of the aspectRatio.<br>
     * Sets fixed aspect ratio to TRUE.
     *
     * @param aspectRatioX int that specifies the new X value of the aspect ratio
     * @param aspectRatioY int that specifies the new Y value of the aspect ratio
     */
    public void setAspectRatio(int aspectRatioX, int aspectRatioY) {
        mCropOverlayView.setAspectRatioX(aspectRatioX);
        mCropOverlayView.setAspectRatioY(aspectRatioY);
        setFixedAspectRatio(true);
    }

    /**
     * Clears set aspect ratio values and sets fixed aspect ratio to FALSE.
     */
    public void clearAspectRatio() {
        mCropOverlayView.setAspectRatioX(1);
        mCropOverlayView.setAspectRatioY(1);
        setFixedAspectRatio(false);
    }

    /**
     * An edge of the crop window will snap to the corresponding edge of a specified bounding box when
     * the crop window edge is less than or equal to this distance (in pixels) away from the bounding
     * box edge. (default: 3dp)
     */
    public void setSnapRadius(float snapRadius) {
        if (snapRadius >= 0) {
            mCropOverlayView.setSnapRadius(snapRadius);
        }
    }

    /**
     * if to show progress bar when image async loading/cropping is in progress.<br>
     * default: true, disable to provide custom progress bar UI.
     */
    public boolean isShowProgressBar() {
        return mShowProgressBar;
    }

    /**
     * if to show progress bar when image async loading/cropping is in progress.<br>
     * default: true, disable to provide custom progress bar UI.
     */
    public void setShowProgressBar(boolean showProgressBar) {
        if (mShowProgressBar != showProgressBar) {
            mShowProgressBar = showProgressBar;
            setProgressBarVisibility();
        }
    }

    /**
     * if to show crop overlay UI what contains the crop window UI surrounded by background over the
     * cropping image.<br>
     * default: true, may disable for animation or frame transition.
     */
    public boolean isShowCropOverlay() {
        return mShowCropOverlay;
    }

    /**
     * if to show crop overlay UI what contains the crop window UI surrounded by background over the
     * cropping image.<br>
     * default: true, may disable for animation or frame transition.
     */
    public void setShowCropOverlay(boolean showCropOverlay) {
        if (mShowCropOverlay != showCropOverlay) {
            mShowCropOverlay = showCropOverlay;
            setCropOverlayVisibility();
        }
    }

    /**
     * if to save bitmap on save instance state.<br>
     * It is best to avoid it by using URI in setting image for cropping.<br>
     * If false the bitmap is not saved and if restore is required to view will be empty, storing the
     * bitmap requires saving it to file which can be expensive. default: false.
     */
    public boolean isSaveBitmapToInstanceState() {
        return mSaveBitmapToInstanceState;
    }

    /**
     * if to save bitmap on save instance state.<br>
     * It is best to avoid it by using URI in setting image for cropping.<br>
     * If false the bitmap is not saved and if restore is required to view will be empty, storing the
     * bitmap requires saving it to file which can be expensive. default: false.
     */
    public void setSaveBitmapToInstanceState(boolean saveBitmapToInstanceState) {
        mSaveBitmapToInstanceState = saveBitmapToInstanceState;
    }

    /**
     * Returns the integer of the imageResource
     */
    public int getImageResource() {
        return mImageResource;
    }

    /**
     * Get the URI of an image that was set by URI, null otherwise.
     */
    public Uri getImageUri() {
        return mLoadedImageUri;
    }

    /**
     * Gets the source Bitmap's dimensions. This represents the largest possible crop rectangle.
     *
     * @return a Rect instance dimensions of the source Bitmap
     */
    public Rect getWholeImageRect() {
        int loadedSampleSize = mLoadedSampleSize;
        Bitmap bitmap = mBitmap;
        if (bitmap == null) {
            return null;
        }

        int orgWidth = bitmap.getWidth() * loadedSampleSize;
        int orgHeight = bitmap.getHeight() * loadedSampleSize;
        return new Rect(0, 0, orgWidth, orgHeight);
    }

    /**
     * Gets the crop window's position relative to the source Bitmap (not the image displayed in the
     * CropImageView) using the original image rotation.
     *
     * @return a Rect instance containing cropped area boundaries of the source Bitmap
     */
    public Rect getCropRect() {
        int loadedSampleSize = mLoadedSampleSize;
        Bitmap bitmap = mBitmap;
        if (bitmap == null) {
            return null;
        }

        // get the points of the crop rectangle adjusted to source bitmap
        float[] points = getCropPoints();

        int orgWidth = bitmap.getWidth() * loadedSampleSize;
        int orgHeight = bitmap.getHeight() * loadedSampleSize;

        // get the rectangle for the points (it may be larger than original if rotation is not stright)
        return BitmapUtils.getRectFromPoints(
                points,
                orgWidth,
                orgHeight,
                mCropOverlayView.isFixAspectRatio(),
                mCropOverlayView.getAspectRatioX(),
                mCropOverlayView.getAspectRatioY());
    }

    /**
     * Gets the crop window's position relative to the parent's view at screen.
     *
     * @return a Rect instance containing cropped area boundaries of the source Bitmap
     */
    public RectF getCropWindowRect() {
        if (mCropOverlayView == null) {
            return null;
        }
        return mCropOverlayView.getCropWindowRect();
    }

    /**
     * Gets the 4 points of crop window's position relative to the source Bitmap (not the image
     * displayed in the CropImageView) using the original image rotation.<br>
     * Note: the 4 points may not be a rectangle if the image was rotates to NOT stright angle (!=
     * 90/180/270).
     *
     * @return 4 points (x0,y0,x1,y1,x2,y2,x3,y3) of cropped area boundaries
     */
    public float[] getCropPoints() {

        // Get crop window position relative to the displayed image.
        RectF cropWindowRect = mCropOverlayView.getCropWindowRect();

        float[] points =
                new float[]{
                        cropWindowRect.left,
                        cropWindowRect.top,
                        cropWindowRect.right,
                        cropWindowRect.top,
                        cropWindowRect.right,
                        cropWindowRect.bottom,
                        cropWindowRect.left,
                        cropWindowRect.bottom
                };

        mImageMatrix.invert(mImageInverseMatrix);
        mImageInverseMatrix.mapPoints(points);

        for (int i = 0; i < points.length; i++) {
            points[i] *= mLoadedSampleSize;
        }

        return points;
    }

    /**
     * Set the crop window position and size to the given rectangle.<br>
     * Image to crop must be first set before invoking this, for async - after complete callback.
     *
     * @param rect window rectangle (position and size) relative to source bitmap
     */
    public void setCropRect(Rect rect) {
        mCropOverlayView.setInitialCropWindowRect(rect);
    }

    /**
     * Reset crop window to initial rectangle.
     */
    public void resetCropRect() {
        mZoom = 1;
        mZoomOffsetX = 0;
        mZoomOffsetY = 0;
        mDegreesRotated = mInitialDegreesRotated;
        mFlipHorizontally = false;
        mFlipVertically = false;
        applyImageMatrix(getWidth(), getHeight(), false, false);
        mCropOverlayView.resetCropWindowRect();
    }

    /**
     * Gets the cropped image based on the current crop window.
     *
     * @return a new Bitmap representing the cropped image
     */
    public Bitmap getCroppedImage() {
        return getCroppedImage(0, 0, RequestSizeOptions.NONE);
    }

    /**
     * Gets the cropped image based on the current crop window.<br>
     * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.
     *
     * @param reqWidth  the width to resize the cropped image to
     * @param reqHeight the height to resize the cropped image to
     * @return a new Bitmap representing the cropped image
     */
    public Bitmap getCroppedImage(int reqWidth, int reqHeight) {
        return getCroppedImage(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
    }

    /**
     * Gets the cropped image based on the current crop window.<br>
     *
     * @param reqWidth  the width to resize the cropped image to (see options)
     * @param reqHeight the height to resize the cropped image to (see options)
     * @param options   the resize method to use, see its documentation
     * @return a new Bitmap representing the cropped image
     */
    public Bitmap getCroppedImage(int reqWidth, int reqHeight, RequestSizeOptions options) {
        Bitmap croppedBitmap = null;
        if (mBitmap != null) {
            mImageView.clearAnimation();

            reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
            reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;

            if (mLoadedImageUri != null
                    && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
                int orgWidth = mBitmap.getWidth() * mLoadedSampleSize;
                int orgHeight = mBitmap.getHeight() * mLoadedSampleSize;
                BitmapUtils.BitmapSampled bitmapSampled =
                        BitmapUtils.cropBitmap(
                                getContext(),
                                mLoadedImageUri,
                                getCropPoints(),
                                mDegreesRotated,
                                orgWidth,
                                orgHeight,
                                mCropOverlayView.isFixAspectRatio(),
                                mCropOverlayView.getAspectRatioX(),
                                mCropOverlayView.getAspectRatioY(),
                                reqWidth,
                                reqHeight,
                                mFlipHorizontally,
                                mFlipVertically);
                croppedBitmap = bitmapSampled.bitmap;
            } else {
                croppedBitmap =
                        BitmapUtils.cropBitmapObjectHandleOOM(
                                mBitmap,
                                getCropPoints(),
                                mDegreesRotated,
                                mCropOverlayView.isFixAspectRatio(),
                                mCropOverlayView.getAspectRatioX(),
                                mCropOverlayView.getAspectRatioY(),
                                mFlipHorizontally,
                                mFlipVertically)
                                .bitmap;
            }

            croppedBitmap = BitmapUtils.resizeBitmap(croppedBitmap, reqWidth, reqHeight, options);
        }

        return croppedBitmap;
    }

    /**
     * Gets the cropped image based on the current crop window.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     */
    public void getCroppedImageAsync() {
        getCroppedImageAsync(0, 0, RequestSizeOptions.NONE);
    }

    /**
     * Gets the cropped image based on the current crop window.<br>
     * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     *
     * @param reqWidth  the width to resize the cropped image to
     * @param reqHeight the height to resize the cropped image to
     */
    public void getCroppedImageAsync(int reqWidth, int reqHeight) {
        getCroppedImageAsync(reqWidth, reqHeight, RequestSizeOptions.RESIZE_INSIDE);
    }

    /**
     * Gets the cropped image based on the current crop window.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     *
     * @param reqWidth  the width to resize the cropped image to (see options)
     * @param reqHeight the height to resize the cropped image to (see options)
     * @param options   the resize method to use, see its documentation
     */
    public void getCroppedImageAsync(int reqWidth, int reqHeight, RequestSizeOptions options) {
        if (mOnCropImageCompleteListener == null) {
            throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
        }
        startCropWorkerTask(reqWidth, reqHeight, options, null, null, 0);
    }

    /**
     * Save the cropped image based on the current crop window to the given uri.<br>
     * Uses JPEG image compression with 90 compression quality.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     *
     * @param saveUri the Android Uri to save the cropped image to
     */
    public void saveCroppedImageAsync(Uri saveUri) {
        saveCroppedImageAsync(saveUri, Bitmap.CompressFormat.JPEG, 90, 0, 0, RequestSizeOptions.NONE);
    }

    /**
     * Save the cropped image based on the current crop window to the given uri.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     *
     * @param saveUri             the Android Uri to save the cropped image to
     * @param saveCompressFormat  the compression format to use when writing the image
     * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
     */
    public void saveCroppedImageAsync(
            Uri saveUri, Bitmap.CompressFormat saveCompressFormat, int saveCompressQuality) {
        saveCroppedImageAsync(
                saveUri, saveCompressFormat, saveCompressQuality, 0, 0, RequestSizeOptions.NONE);
    }

    /**
     * Save the cropped image based on the current crop window to the given uri.<br>
     * Uses {@link RequestSizeOptions#RESIZE_INSIDE} option.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     *
     * @param saveUri             the Android Uri to save the cropped image to
     * @param saveCompressFormat  the compression format to use when writing the image
     * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
     * @param reqWidth            the width to resize the cropped image to
     * @param reqHeight           the height to resize the cropped image to
     */
    public void saveCroppedImageAsync(
            Uri saveUri,
            Bitmap.CompressFormat saveCompressFormat,
            int saveCompressQuality,
            int reqWidth,
            int reqHeight) {
        saveCroppedImageAsync(
                saveUri,
                saveCompressFormat,
                saveCompressQuality,
                reqWidth,
                reqHeight,
                RequestSizeOptions.RESIZE_INSIDE);
    }

    /**
     * Save the cropped image based on the current crop window to the given uri.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     *
     * @param saveUri             the Android Uri to save the cropped image to
     * @param saveCompressFormat  the compression format to use when writing the image
     * @param saveCompressQuality the quality (if applicable) to use when writing the image (0 - 100)
     * @param reqWidth            the width to resize the cropped image to (see options)
     * @param reqHeight           the height to resize the cropped image to (see options)
     * @param options             the resize method to use, see its documentation
     */
    public void saveCroppedImageAsync(
            Uri saveUri,
            Bitmap.CompressFormat saveCompressFormat,
            int saveCompressQuality,
            int reqWidth,
            int reqHeight,
            RequestSizeOptions options) {
        if (mOnCropImageCompleteListener == null) {
            throw new IllegalArgumentException("mOnCropImageCompleteListener is not set");
        }
        startCropWorkerTask(
                reqWidth, reqHeight, options, saveUri, saveCompressFormat, saveCompressQuality);
    }

    /**
     * Set the callback t
     */
    public void setOnSetCropOverlayReleasedListener(OnSetCropOverlayReleasedListener listener) {
        mOnCropOverlayReleasedListener = listener;
    }

    /**
     * Set the callback when the cropping is moved
     */
    public void setOnSetCropOverlayMovedListener(OnSetCropOverlayMovedListener listener) {
        mOnSetCropOverlayMovedListener = listener;
    }

    /**
     * Set the callback when the crop window is changed
     */
    public void setOnCropWindowChangedListener(OnSetCropWindowChangeListener listener) {
        mOnSetCropWindowChangeListener = listener;
    }

    /**
     * Set the callback to be invoked when image async loading ({@link #setImageUriAsync(Uri)}) is
     * complete (successful or failed).
     */
    public void setOnSetImageUriCompleteListener(OnSetImageUriCompleteListener listener) {
        mOnSetImageUriCompleteListener = listener;
    }

    /**
     * Set the callback to be invoked when image async cropping image ({@link #getCroppedImageAsync()}
     * or {@link #saveCroppedImageAsync(Uri)}) is complete (successful or failed).
     */
    public void setOnCropImageCompleteListener(OnCropImageCompleteListener listener) {
        mOnCropImageCompleteListener = listener;
    }

    /**
     * Sets a Bitmap as the content of the CropImageView.
     *
     * @param bitmap the Bitmap to set
     */
    public void setImageBitmap(Bitmap bitmap) {
        mCropOverlayView.setInitialCropWindowRect(null);
        setBitmap(bitmap, 0, null, 1, 0);
    }

    /**
     * Sets a Bitmap and initializes the image rotation according to the EXIT data.<br>
     * <br>
     * The EXIF can be retrieved by doing the following: <code>
     * ExifInterface exif = new ExifInterface(path);</code>
     *
     * @param bitmap the original bitmap to set; if null, this
     * @param exif   the EXIF information about this bitmap; may be null
     */
    public void setImageBitmap(Bitmap bitmap, ExifInterface exif) {
        Bitmap setBitmap;
        int degreesRotated = 0;
        if (bitmap != null && exif != null) {
            BitmapUtils.RotateBitmapResult result = BitmapUtils.rotateBitmapByExif(bitmap, exif);
            setBitmap = result.bitmap;
            degreesRotated = result.degrees;
            mInitialDegreesRotated = result.degrees;
        } else {
            setBitmap = bitmap;
        }
        mCropOverlayView.setInitialCropWindowRect(null);
        setBitmap(setBitmap, 0, null, 1, degreesRotated);
    }

    /**
     * Sets a Drawable as the content of the CropImageView.
     *
     * @param resId the drawable resource ID to set
     */
    public void setImageResource(int resId) {
        if (resId != 0) {
            mCropOverlayView.setInitialCropWindowRect(null);
            Bitmap bitmap = BitmapFactory.decodeResource(getResources(), resId);
            setBitmap(bitmap, resId, null, 1, 0);
        }
    }

    /**
     * Sets a bitmap loaded from the given Android URI as the content of the CropImageView.<br>
     * Can be used with URI from gallery or camera source.<br>
     * Will rotate the image by exif data.<br>
     *
     * @param uri the URI to load the image from
     */
    public void setImageUriAsync(Uri uri) {
        if (uri != null) {
            BitmapLoadingWorkerTask currentTask =
                    mBitmapLoadingWorkerTask != null ? mBitmapLoadingWorkerTask.get() : null;
            if (currentTask != null) {
                // cancel previous loading (no check if the same URI because camera URI can be the same for
                // different images)
                currentTask.cancel(true);
            }

            // either no existing task is working or we canceled it, need to load new URI
            clearImageInt();
            mRestoreCropWindowRect = null;
            mRestoreDegreesRotated = 0;
            mCropOverlayView.setInitialCropWindowRect(null);
            mBitmapLoadingWorkerTask = new WeakReference<BitmapLoadingWorkerTask>(new BitmapLoadingWorkerTask(this, uri));
            mBitmapLoadingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            setProgressBarVisibility();
        }
    }

    /**
     * Clear the current image set for cropping.
     */
    public void clearImage() {
        clearImageInt();
        mCropOverlayView.setInitialCropWindowRect(null);
    }

    /**
     * Rotates image by the specified number of degrees clockwise.<br>
     * Negative values represent counter-clockwise rotations.
     *
     * @param degrees Integer specifying the number of degrees to rotate.
     */
    public void rotateImage(int degrees) {
        if (mBitmap != null) {
            // Force degrees to be a non-zero value between 0 and 360 (inclusive)
            if (degrees < 0) {
                degrees = (degrees % 360) + 360;
            } else {
                degrees = degrees % 360;
            }

            boolean flipAxes =
                    !mCropOverlayView.isFixAspectRatio()
                            && ((degrees > 45 && degrees < 135) || (degrees > 215 && degrees < 305));
            BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());
            float halfWidth = (flipAxes ? BitmapUtils.RECT.height() : BitmapUtils.RECT.width()) / 2f;
            float halfHeight = (flipAxes ? BitmapUtils.RECT.width() : BitmapUtils.RECT.height()) / 2f;
            if (flipAxes) {
                boolean isFlippedHorizontally = mFlipHorizontally;
                mFlipHorizontally = mFlipVertically;
                mFlipVertically = isFlippedHorizontally;
            }

            mImageMatrix.invert(mImageInverseMatrix);

            BitmapUtils.POINTS[0] = BitmapUtils.RECT.centerX();
            BitmapUtils.POINTS[1] = BitmapUtils.RECT.centerY();
            BitmapUtils.POINTS[2] = 0;
            BitmapUtils.POINTS[3] = 0;
            BitmapUtils.POINTS[4] = 1;
            BitmapUtils.POINTS[5] = 0;
            mImageInverseMatrix.mapPoints(BitmapUtils.POINTS);

            // This is valid because degrees is not negative.
            mDegreesRotated = (mDegreesRotated + degrees) % 360;

            applyImageMatrix(getWidth(), getHeight(), true, false);

            // adjust the zoom so the crop window size remains the same even after image scale change
            mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);
            mZoom /=
                    Math.sqrt(
                            Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
                                    + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
            mZoom = Math.max(mZoom, 1);

            applyImageMatrix(getWidth(), getHeight(), true, false);

            mImageMatrix.mapPoints(BitmapUtils.POINTS2, BitmapUtils.POINTS);

            // adjust the width/height by the changes in scaling to the image
            double change =
                    Math.sqrt(
                            Math.pow(BitmapUtils.POINTS2[4] - BitmapUtils.POINTS2[2], 2)
                                    + Math.pow(BitmapUtils.POINTS2[5] - BitmapUtils.POINTS2[3], 2));
            halfWidth *= change;
            halfHeight *= change;

            // calculate the new crop window rectangle to center in the same location and have proper
            // width/height
            BitmapUtils.RECT.set(
                    BitmapUtils.POINTS2[0] - halfWidth,
                    BitmapUtils.POINTS2[1] - halfHeight,
                    BitmapUtils.POINTS2[0] + halfWidth,
                    BitmapUtils.POINTS2[1] + halfHeight);

            mCropOverlayView.resetCropOverlayView();
            mCropOverlayView.setCropWindowRect(BitmapUtils.RECT);
            applyImageMatrix(getWidth(), getHeight(), true, false);
            handleCropWindowChanged(false, false);

            // make sure the crop window rectangle is within the cropping image bounds after all the
            // changes
            mCropOverlayView.fixCurrentCropWindowRect();
        }
    }

    /**
     * Flips the image horizontally.
     */
    public void flipImageHorizontally() {
        mFlipHorizontally = !mFlipHorizontally;
        applyImageMatrix(getWidth(), getHeight(), true, false);
    }

    /**
     * Flips the image vertically.
     */
    public void flipImageVertically() {
        mFlipVertically = !mFlipVertically;
        applyImageMatrix(getWidth(), getHeight(), true, false);
    }

    // region: Private methods

    /**
     * On complete of the async bitmap loading by {@link #setImageUriAsync(Uri)} set the result to the
     * widget if still relevant and call listener if set.
     *
     * @param result the result of bitmap loading
     */
    void onSetImageUriAsyncComplete(BitmapLoadingWorkerTask.Result result) {

        mBitmapLoadingWorkerTask = null;
        setProgressBarVisibility();

        if (result.error == null) {
            mInitialDegreesRotated = result.degreesRotated;
            setBitmap(result.bitmap, 0, result.uri, result.loadSampleSize, result.degreesRotated);
        }

        OnSetImageUriCompleteListener listener = mOnSetImageUriCompleteListener;
        if (listener != null) {
            listener.onSetImageUriComplete(this, result.uri, result.error);
        }
    }

    /**
     * On complete of the async bitmap cropping by {@link #getCroppedImageAsync()} call listener if
     * set.
     *
     * @param result the result of bitmap cropping
     */
    void onImageCroppingAsyncComplete(BitmapCroppingWorkerTask.Result result) {

        mBitmapCroppingWorkerTask = null;
        setProgressBarVisibility();

        OnCropImageCompleteListener listener = mOnCropImageCompleteListener;
        if (listener != null) {
            CropResult cropResult =
                    new CropResult(
                            mBitmap,
                            mLoadedImageUri,
                            result.bitmap,
                            result.uri,
                            result.error,
                            getCropPoints(),
                            getCropRect(),
                            getWholeImageRect(),
                            getRotatedDegrees(),
                            result.sampleSize);
            listener.onCropImageComplete(this, cropResult);
        }
    }

    /**
     * Set the given bitmap to be used in for cropping<br>
     * Optionally clear full if the bitmap is new, or partial clear if the bitmap has been
     * manipulated.
     */
    private void setBitmap(
            Bitmap bitmap, int imageResource, Uri imageUri, int loadSampleSize, int degreesRotated) {
        if (mBitmap == null || !mBitmap.equals(bitmap)) {

            mImageView.clearAnimation();

            clearImageInt();

            mBitmap = bitmap;
            mImageView.setImageBitmap(mBitmap);

            mLoadedImageUri = imageUri;
            mImageResource = imageResource;
            mLoadedSampleSize = loadSampleSize;
            mDegreesRotated = degreesRotated;

            applyImageMatrix(getWidth(), getHeight(), true, false);

            if (mCropOverlayView != null) {
                mCropOverlayView.resetCropOverlayView();
                setCropOverlayVisibility();
            }
        }
    }

    /**
     * Clear the current image set for cropping.<br>
     * Full clear will also clear the data of the set image like Uri or Resource id while partial
     * clear will only clear the bitmap and recycle if required.
     */
    private void clearImageInt() {

        // if we allocated the bitmap, release it as fast as possible
        if (mBitmap != null && (mImageResource > 0 || mLoadedImageUri != null)) {
            mBitmap.recycle();
        }
        mBitmap = null;

        // clean the loaded image flags for new image
        mImageResource = 0;
        mLoadedImageUri = null;
        mLoadedSampleSize = 1;
        mDegreesRotated = 0;
        mZoom = 1;
        mZoomOffsetX = 0;
        mZoomOffsetY = 0;
        mImageMatrix.reset();
        mSaveInstanceStateBitmapUri = null;

        mImageView.setImageBitmap(null);

        setCropOverlayVisibility();
    }

    /**
     * Gets the cropped image based on the current crop window.<br>
     * If (reqWidth,reqHeight) is given AND image is loaded from URI cropping will try to use sample
     * size to fit in the requested width and height down-sampling if possible - optimization to get
     * best size to quality.<br>
     * The result will be invoked to listener set by {@link
     * #setOnCropImageCompleteListener(OnCropImageCompleteListener)}.
     *
     * @param reqWidth            the width to resize the cropped image to (see options)
     * @param reqHeight           the height to resize the cropped image to (see options)
     * @param options             the resize method to use on the cropped bitmap
     * @param saveUri             optional: to save the cropped image to
     * @param saveCompressFormat  if saveUri is given, the given compression will be used for saving
     *                            the image
     * @param saveCompressQuality if saveUri is given, the given quality will be used for the
     *                            compression.
     */
    public void startCropWorkerTask(
            int reqWidth,
            int reqHeight,
            RequestSizeOptions options,
            Uri saveUri,
            Bitmap.CompressFormat saveCompressFormat,
            int saveCompressQuality) {
        Bitmap bitmap = mBitmap;
        if (bitmap != null) {
            mImageView.clearAnimation();

            BitmapCroppingWorkerTask currentTask =
                    mBitmapCroppingWorkerTask != null ? mBitmapCroppingWorkerTask.get() : null;
            if (currentTask != null) {
                // cancel previous cropping
                currentTask.cancel(true);
            }

            reqWidth = options != RequestSizeOptions.NONE ? reqWidth : 0;
            reqHeight = options != RequestSizeOptions.NONE ? reqHeight : 0;

            int orgWidth = bitmap.getWidth() * mLoadedSampleSize;
            int orgHeight = bitmap.getHeight() * mLoadedSampleSize;
            if (mLoadedImageUri != null
                    && (mLoadedSampleSize > 1 || options == RequestSizeOptions.SAMPLING)) {
                mBitmapCroppingWorkerTask =
                        new WeakReference<BitmapCroppingWorkerTask>(
                                new BitmapCroppingWorkerTask(
                                        this,
                                        mLoadedImageUri,
                                        getCropPoints(),
                                        mDegreesRotated,
                                        orgWidth,
                                        orgHeight,
                                        mCropOverlayView.isFixAspectRatio(),
                                        mCropOverlayView.getAspectRatioX(),
                                        mCropOverlayView.getAspectRatioY(),
                                        reqWidth,
                                        reqHeight,
                                        mFlipHorizontally,
                                        mFlipVertically,
                                        options,
                                        saveUri,
                                        saveCompressFormat,
                                        saveCompressQuality));
            } else {
                mBitmapCroppingWorkerTask =
                        new WeakReference<BitmapCroppingWorkerTask>(
                                new BitmapCroppingWorkerTask(
                                        this,
                                        bitmap,
                                        getCropPoints(),
                                        mDegreesRotated,
                                        mCropOverlayView.isFixAspectRatio(),
                                        mCropOverlayView.getAspectRatioX(),
                                        mCropOverlayView.getAspectRatioY(),
                                        reqWidth,
                                        reqHeight,
                                        mFlipHorizontally,
                                        mFlipVertically,
                                        options,
                                        saveUri,
                                        saveCompressFormat,
                                        saveCompressQuality));
            }
            mBitmapCroppingWorkerTask.get().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
            setProgressBarVisibility();
        }
    }

    @Override
    public Parcelable onSaveInstanceState() {
        if (mLoadedImageUri == null && mBitmap == null && mImageResource < 1) {
            return super.onSaveInstanceState();
        }

        Bundle bundle = new Bundle();
        Uri imageUri = mLoadedImageUri;
        if (mSaveBitmapToInstanceState && imageUri == null && mImageResource < 1) {
            mSaveInstanceStateBitmapUri =
                    imageUri =
                            BitmapUtils.writeTempStateStoreBitmap(
                                    getContext(), mBitmap, mSaveInstanceStateBitmapUri);
        }
        if (imageUri != null && mBitmap != null) {
            String key = UUID.randomUUID().toString();
            BitmapUtils.mStateBitmap = new Pair<String, WeakReference<Bitmap>>(key, new WeakReference<Bitmap>(mBitmap));
            bundle.putString("LOADED_IMAGE_STATE_BITMAP_KEY", key);
        }
        if (mBitmapLoadingWorkerTask != null) {
            BitmapLoadingWorkerTask task = mBitmapLoadingWorkerTask.get();
            if (task != null) {
                bundle.putParcelable("LOADING_IMAGE_URI", task.getUri());
            }
        }
        bundle.putParcelable("instanceState", super.onSaveInstanceState());
        bundle.putParcelable("LOADED_IMAGE_URI", imageUri);
        bundle.putInt("LOADED_IMAGE_RESOURCE", mImageResource);
        bundle.putInt("LOADED_SAMPLE_SIZE", mLoadedSampleSize);
        bundle.putInt("DEGREES_ROTATED", mDegreesRotated);
        bundle.putParcelable("INITIAL_CROP_RECT", mCropOverlayView.getInitialCropWindowRect());

        BitmapUtils.RECT.set(mCropOverlayView.getCropWindowRect());

        mImageMatrix.invert(mImageInverseMatrix);
        mImageInverseMatrix.mapRect(BitmapUtils.RECT);

        bundle.putParcelable("CROP_WINDOW_RECT", BitmapUtils.RECT);
        bundle.putString("CROP_SHAPE", mCropOverlayView.getCropShape().name());
        bundle.putBoolean("CROP_AUTO_ZOOM_ENABLED", mAutoZoomEnabled);
        bundle.putInt("CROP_MAX_ZOOM", mMaxZoom);
        bundle.putBoolean("CROP_FLIP_HORIZONTALLY", mFlipHorizontally);
        bundle.putBoolean("CROP_FLIP_VERTICALLY", mFlipVertically);

        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {

        if (state instanceof Bundle) {
            Bundle bundle = (Bundle) state;

            // prevent restoring state if already set by outside code
            if (mBitmapLoadingWorkerTask == null
                    && mLoadedImageUri == null
                    && mBitmap == null
                    && mImageResource == 0) {

                Uri uri = bundle.getParcelable("LOADED_IMAGE_URI");
                if (uri != null) {
                    String key = bundle.getString("LOADED_IMAGE_STATE_BITMAP_KEY");
                    if (key != null) {
                        Bitmap stateBitmap =
                                BitmapUtils.mStateBitmap != null && BitmapUtils.mStateBitmap.first.equals(key)
                                        ? BitmapUtils.mStateBitmap.second.get()
                                        : null;
                        BitmapUtils.mStateBitmap = null;
                        if (stateBitmap != null && !stateBitmap.isRecycled()) {
                            setBitmap(stateBitmap, 0, uri, bundle.getInt("LOADED_SAMPLE_SIZE"), 0);
                        }
                    }
                    if (mLoadedImageUri == null) {
                        setImageUriAsync(uri);
                    }
                } else {
                    int resId = bundle.getInt("LOADED_IMAGE_RESOURCE");
                    if (resId > 0) {
                        setImageResource(resId);
                    } else {
                        uri = bundle.getParcelable("LOADING_IMAGE_URI");
                        if (uri != null) {
                            setImageUriAsync(uri);
                        }
                    }
                }

                mDegreesRotated = mRestoreDegreesRotated = bundle.getInt("DEGREES_ROTATED");

                Rect initialCropRect = bundle.getParcelable("INITIAL_CROP_RECT");
                if (initialCropRect != null
                        && (initialCropRect.width() > 0 || initialCropRect.height() > 0)) {
                    mCropOverlayView.setInitialCropWindowRect(initialCropRect);
                }

                RectF cropWindowRect = bundle.getParcelable("CROP_WINDOW_RECT");
                if (cropWindowRect != null && (cropWindowRect.width() > 0 || cropWindowRect.height() > 0)) {
                    mRestoreCropWindowRect = cropWindowRect;
                }

                mCropOverlayView.setCropShape(CropShape.valueOf(bundle.getString("CROP_SHAPE")));

                mAutoZoomEnabled = bundle.getBoolean("CROP_AUTO_ZOOM_ENABLED");
                mMaxZoom = bundle.getInt("CROP_MAX_ZOOM");

                mFlipHorizontally = bundle.getBoolean("CROP_FLIP_HORIZONTALLY");
                mFlipVertically = bundle.getBoolean("CROP_FLIP_VERTICALLY");
            }

            super.onRestoreInstanceState(bundle.getParcelable("instanceState"));
        } else {
            super.onRestoreInstanceState(state);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (mBitmap != null) {

            // Bypasses a baffling bug when used within a ScrollView, where heightSize is set to 0.
            if (heightSize == 0) {
                heightSize = mBitmap.getHeight();
            }

            int desiredWidth;
            int desiredHeight;

            double viewToBitmapWidthRatio = Double.POSITIVE_INFINITY;
            double viewToBitmapHeightRatio = Double.POSITIVE_INFINITY;

            // Checks if either width or height needs to be fixed
            if (widthSize < mBitmap.getWidth()) {
                viewToBitmapWidthRatio = (double) widthSize / (double) mBitmap.getWidth();
            }
            if (heightSize < mBitmap.getHeight()) {
                viewToBitmapHeightRatio = (double) heightSize / (double) mBitmap.getHeight();
            }

            // If either needs to be fixed, choose smallest ratio and calculate from there
            if (viewToBitmapWidthRatio != Double.POSITIVE_INFINITY
                    || viewToBitmapHeightRatio != Double.POSITIVE_INFINITY) {
                if (viewToBitmapWidthRatio <= viewToBitmapHeightRatio) {
                    desiredWidth = widthSize;
                    desiredHeight = (int) (mBitmap.getHeight() * viewToBitmapWidthRatio);
                } else {
                    desiredHeight = heightSize;
                    desiredWidth = (int) (mBitmap.getWidth() * viewToBitmapHeightRatio);
                }
            } else {
                // Otherwise, the picture is within frame layout bounds. Desired width is simply picture
                // size
                desiredWidth = mBitmap.getWidth();
                desiredHeight = mBitmap.getHeight();
            }

            int width = getOnMeasureSpec(widthMode, widthSize, desiredWidth);
            int height = getOnMeasureSpec(heightMode, heightSize, desiredHeight);

            mLayoutWidth = width;
            mLayoutHeight = height;

            setMeasuredDimension(mLayoutWidth, mLayoutHeight);

        } else {
            setMeasuredDimension(widthSize, heightSize);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        super.onLayout(changed, l, t, r, b);

        if (mLayoutWidth > 0 && mLayoutHeight > 0) {
            // Gets original parameters, and creates the new parameters
            ViewGroup.LayoutParams origParams = this.getLayoutParams();
            origParams.width = mLayoutWidth;
            origParams.height = mLayoutHeight;
            setLayoutParams(origParams);

            if (mBitmap != null) {
                applyImageMatrix(r - l, b - t, true, false);

                // after state restore we want to restore the window crop, possible only after widget size
                // is known
                if (mRestoreCropWindowRect != null) {
                    if (mRestoreDegreesRotated != mInitialDegreesRotated) {
                        mDegreesRotated = mRestoreDegreesRotated;
                        applyImageMatrix(r - l, b - t, true, false);
                    }
                    mImageMatrix.mapRect(mRestoreCropWindowRect);
                    mCropOverlayView.setCropWindowRect(mRestoreCropWindowRect);
                    handleCropWindowChanged(false, false);
                    mCropOverlayView.fixCurrentCropWindowRect();
                    mRestoreCropWindowRect = null;
                } else if (mSizeChanged) {
                    mSizeChanged = false;
                    handleCropWindowChanged(false, false);
                }
            } else {
                updateImageBounds(true);
            }
        } else {
            updateImageBounds(true);
        }
    }

    /**
     * Detect size change to handle auto-zoom using {@link #handleCropWindowChanged(boolean, boolean)}
     * in {@link #layout(int, int, int, int)}.
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mSizeChanged = oldw > 0 && oldh > 0;
    }

    /**
     * Handle crop window change to:<br>
     * 1. Execute auto-zoom-in/out depending on the area covered of cropping window relative to the
     * available view area.<br>
     * 2. Slide the zoomed sub-area if the cropping window is outside of the visible view sub-area.
     * <br>
     *
     * @param inProgress is the crop window change is still in progress by the user
     * @param animate    if to animate the change to the image matrix, or set it directly
     */
    private void handleCropWindowChanged(boolean inProgress, boolean animate) {
        int width = getWidth();
        int height = getHeight();
        if (mBitmap != null && width > 0 && height > 0) {

            RectF cropRect = mCropOverlayView.getCropWindowRect();
            if (inProgress) {
                if (cropRect.left < 0
                        || cropRect.top < 0
                        || cropRect.right > width
                        || cropRect.bottom > height) {
                    applyImageMatrix(width, height, false, false);
                }
            } else if (mAutoZoomEnabled || mZoom > 1) {
                float newZoom = 0;
                // keep the cropping window covered area to 50%-65% of zoomed sub-area
                if (mZoom < mMaxZoom
                        && cropRect.width() < width * 0.5f
                        && cropRect.height() < height * 0.5f) {
                    newZoom =
                            Math.min(
                                    mMaxZoom,
                                    Math.min(
                                            width / (cropRect.width() / mZoom / 0.64f),
                                            height / (cropRect.height() / mZoom / 0.64f)));
                }
                if (mZoom > 1 && (cropRect.width() > width * 0.65f || cropRect.height() > height * 0.65f)) {
                    newZoom =
                            Math.max(
                                    1,
                                    Math.min(
                                            width / (cropRect.width() / mZoom / 0.51f),
                                            height / (cropRect.height() / mZoom / 0.51f)));
                }
                if (!mAutoZoomEnabled) {
                    newZoom = 1;
                }

                if (newZoom > 0 && newZoom != mZoom) {
                    if (animate) {
                        if (mAnimation == null) {
                            // lazy create animation single instance
                            mAnimation = new CropImageAnimation(mImageView, mCropOverlayView);
                        }
                        // set the state for animation to start from
                        mAnimation.setStartState(mImagePoints, mImageMatrix);
                    }

                    mZoom = newZoom;

                    applyImageMatrix(width, height, true, animate);
                }
            }
            if (mOnSetCropWindowChangeListener != null && !inProgress) {
                mOnSetCropWindowChangeListener.onCropWindowChanged();
            }
        }
    }

    /**
     * Apply matrix to handle the image inside the image view.
     *
     * @param width  the width of the image view
     * @param height the height of the image view
     */
    private void applyImageMatrix(float width, float height, boolean center, boolean animate) {
        if (mBitmap != null && width > 0 && height > 0) {

            mImageMatrix.invert(mImageInverseMatrix);
            RectF cropRect = mCropOverlayView.getCropWindowRect();
            mImageInverseMatrix.mapRect(cropRect);

            mImageMatrix.reset();

            // move the image to the center of the image view first so we can manipulate it from there
            mImageMatrix.postTranslate(
                    (width - mBitmap.getWidth()) / 2, (height - mBitmap.getHeight()) / 2);
            mapImagePointsByImageMatrix();

            // rotate the image the required degrees from center of image
            if (mDegreesRotated > 0) {
                mImageMatrix.postRotate(
                        mDegreesRotated,
                        BitmapUtils.getRectCenterX(mImagePoints),
                        BitmapUtils.getRectCenterY(mImagePoints));
                mapImagePointsByImageMatrix();
            }

            // scale the image to the image view, image rect transformed to know new width/height
            float scale =
                    Math.min(
                            width / BitmapUtils.getRectWidth(mImagePoints),
                            height / BitmapUtils.getRectHeight(mImagePoints));
            if (mScaleType == ScaleType.FIT_CENTER
                    || (mScaleType == ScaleType.CENTER_INSIDE && scale < 1)
                    || (scale > 1 && mAutoZoomEnabled)) {
                mImageMatrix.postScale(
                        scale,
                        scale,
                        BitmapUtils.getRectCenterX(mImagePoints),
                        BitmapUtils.getRectCenterY(mImagePoints));
                mapImagePointsByImageMatrix();
            }

            // scale by the current zoom level
            float scaleX = mFlipHorizontally ? -mZoom : mZoom;
            float scaleY = mFlipVertically ? -mZoom : mZoom;
            mImageMatrix.postScale(
                    scaleX,
                    scaleY,
                    BitmapUtils.getRectCenterX(mImagePoints),
                    BitmapUtils.getRectCenterY(mImagePoints));
            mapImagePointsByImageMatrix();

            mImageMatrix.mapRect(cropRect);

            if (center) {
                // set the zoomed area to be as to the center of cropping window as possible
                mZoomOffsetX =
                        width > BitmapUtils.getRectWidth(mImagePoints)
                                ? 0
                                : Math.max(
                                Math.min(
                                        width / 2 - cropRect.centerX(), -BitmapUtils.getRectLeft(mImagePoints)),
                                getWidth() - BitmapUtils.getRectRight(mImagePoints))
                                / scaleX;
                mZoomOffsetY =
                        height > BitmapUtils.getRectHeight(mImagePoints)
                                ? 0
                                : Math.max(
                                Math.min(
                                        height / 2 - cropRect.centerY(), -BitmapUtils.getRectTop(mImagePoints)),
                                getHeight() - BitmapUtils.getRectBottom(mImagePoints))
                                / scaleY;
            } else {
                // adjust the zoomed area so the crop window rectangle will be inside the area in case it
                // was moved outside
                mZoomOffsetX =
                        Math.min(Math.max(mZoomOffsetX * scaleX, -cropRect.left), -cropRect.right + width)
                                / scaleX;
                mZoomOffsetY =
                        Math.min(Math.max(mZoomOffsetY * scaleY, -cropRect.top), -cropRect.bottom + height)
                                / scaleY;
            }

            // apply to zoom offset translate and update the crop rectangle to offset correctly
            mImageMatrix.postTranslate(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
            cropRect.offset(mZoomOffsetX * scaleX, mZoomOffsetY * scaleY);
            mCropOverlayView.setCropWindowRect(cropRect);
            mapImagePointsByImageMatrix();
            mCropOverlayView.invalidate();

            // set matrix to apply
            if (animate) {
                // set the state for animation to end in, start animation now
                mAnimation.setEndState(mImagePoints, mImageMatrix);
                mImageView.startAnimation(mAnimation);
            } else {
                mImageView.setImageMatrix(mImageMatrix);
            }

            // update the image rectangle in the crop overlay
            updateImageBounds(false);
        }
    }

    /**
     * Adjust the given image rectangle by image transformation matrix to know the final rectangle of
     * the image.<br>
     * To get the proper rectangle it must be first reset to original image rectangle.
     */
    private void mapImagePointsByImageMatrix() {
        mImagePoints[0] = 0;
        mImagePoints[1] = 0;
        mImagePoints[2] = mBitmap.getWidth();
        mImagePoints[3] = 0;
        mImagePoints[4] = mBitmap.getWidth();
        mImagePoints[5] = mBitmap.getHeight();
        mImagePoints[6] = 0;
        mImagePoints[7] = mBitmap.getHeight();
        mImageMatrix.mapPoints(mImagePoints);
        mScaleImagePoints[0] = 0;
        mScaleImagePoints[1] = 0;
        mScaleImagePoints[2] = 100;
        mScaleImagePoints[3] = 0;
        mScaleImagePoints[4] = 100;
        mScaleImagePoints[5] = 100;
        mScaleImagePoints[6] = 0;
        mScaleImagePoints[7] = 100;
        mImageMatrix.mapPoints(mScaleImagePoints);
    }

    /**
     * Determines the specs for the onMeasure function. Calculates the width or height depending on
     * the mode.
     *
     * @param measureSpecMode The mode of the measured width or height.
     * @param measureSpecSize The size of the measured width or height.
     * @param desiredSize     The desired size of the measured width or height.
     * @return The final size of the width or height.
     */
    private static int getOnMeasureSpec(int measureSpecMode, int measureSpecSize, int desiredSize) {

        // Measure Width
        int spec;
        if (measureSpecMode == MeasureSpec.EXACTLY) {
            // Must be this size
            spec = measureSpecSize;
        } else if (measureSpecMode == MeasureSpec.AT_MOST) {
            // Can't be bigger than...; match_parent value
            spec = Math.min(desiredSize, measureSpecSize);
        } else {
            // Be whatever you want; wrap_content
            spec = desiredSize;
        }

        return spec;
    }

    /**
     * Set visibility of crop overlay to hide it when there is no image or specificly set by client.
     */
    private void setCropOverlayVisibility() {
        if (mCropOverlayView != null) {
            mCropOverlayView.setVisibility(mShowCropOverlay && mBitmap != null ? VISIBLE : INVISIBLE);
        }
    }

    /**
     * Set visibility of progress bar when async loading/cropping is in process and show is enabled.
     */
    private void setProgressBarVisibility() {
        boolean visible =
                mShowProgressBar
                        && (mBitmap == null && mBitmapLoadingWorkerTask != null
                        || mBitmapCroppingWorkerTask != null);
        mProgressBar.setVisibility(visible ? VISIBLE : INVISIBLE);
    }

    /**
     * Update the scale factor between the actual image bitmap and the shown image.<br>
     */
    private void updateImageBounds(boolean clear) {
        if (mBitmap != null && !clear) {

            // Get the scale factor between the actual Bitmap dimensions and the displayed dimensions for
            // width/height.
            float scaleFactorWidth =
                    100f * mLoadedSampleSize / BitmapUtils.getRectWidth(mScaleImagePoints);
            float scaleFactorHeight =
                    100f * mLoadedSampleSize / BitmapUtils.getRectHeight(mScaleImagePoints);
            mCropOverlayView.setCropWindowLimits(
                    getWidth(), getHeight(), scaleFactorWidth, scaleFactorHeight);
        }

        // set the bitmap rectangle and update the crop window after scale factor is set
        mCropOverlayView.setBounds(clear ? null : mImagePoints, getWidth(), getHeight());
    }
    // endregion

    // region: Inner class: CropShape

    /**
     * The possible cropping area shape.<br>
     * To set square/circle crop shape set aspect ratio to 1:1.
     */
    public enum CropShape {
        RECTANGLE,
        OVAL
    }
    // endregion

    // region: Inner class: ScaleType

    /**
     * Options for scaling the bounds of cropping image to the bounds of Crop Image View.<br>
     * Note: Some options are affected by auto-zoom, if enabled.
     */
    public enum ScaleType {

        /**
         * Scale the image uniformly (maintain the image's aspect ratio) to fit in crop image view.<br>
         * The largest dimension will be equals to crop image view and the second dimension will be
         * smaller.
         */
        FIT_CENTER,

        /**
         * Center the image in the view, but perform no scaling.<br>
         * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
         * will be scaled uniformly to fit the crop image view.
         */
        CENTER,

        /**
         * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
         * and height) of the image will be equal to or <b>larger</b> than the corresponding dimension
         * of the view (minus padding).<br>
         * The image is then centered in the view.
         */
        CENTER_CROP,

        /**
         * Scale the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
         * and height) of the image will be equal to or <b>less</b> than the corresponding dimension of
         * the view (minus padding).<br>
         * The image is then centered in the view.<br>
         * Note: If auto-zoom is enabled and the source image is smaller than crop image view then it
         * will be scaled uniformly to fit the crop image view.
         */
        CENTER_INSIDE
    }
    // endregion

    // region: Inner class: Guidelines

    /**
     * The possible guidelines showing types.
     */
    public enum Guidelines {
        /**
         * Never show
         */
        OFF,

        /**
         * Show when crop move action is live
         */
        ON_TOUCH,

        /**
         * Always show
         */
        ON
    }
    // endregion

    // region: Inner class: RequestSizeOptions

    /**
     * Possible options for handling requested width/height for cropping.
     */
    public enum RequestSizeOptions {

        /**
         * No resize/sampling is done unless required for memory management (OOM).
         */
        NONE,

        /**
         * Only sample the image during loading (if image set using URI) so the smallest of the image
         * dimensions will be between the requested size and x2 requested size.<br>
         * NOTE: resulting image will not be exactly requested width/height see: <a
         * href="http://developer.android.com/training/displaying-bitmaps/load-bitmap.html">Loading
         * Large Bitmaps Efficiently</a>.
         */
        SAMPLING,

        /**
         * Resize the image uniformly (maintain the image's aspect ratio) so that both dimensions (width
         * and height) of the image will be equal to or <b>less</b> than the corresponding requested
         * dimension.<br>
         * If the image is smaller than the requested size it will NOT change.
         */
        RESIZE_INSIDE,

        /**
         * Resize the image uniformly (maintain the image's aspect ratio) to fit in the given
         * width/height.<br>
         * The largest dimension will be equals to the requested and the second dimension will be
         * smaller.<br>
         * If the image is smaller than the requested size it will enlarge it.
         */
        RESIZE_FIT,

        /**
         * Resize the image to fit exactly in the given width/height.<br>
         * This resize method does NOT preserve aspect ratio.<br>
         * If the image is smaller than the requested size it will enlarge it.
         */
        RESIZE_EXACT
    }
    // endregion

    // region: Inner class: OnSetImageUriCompleteListener

    /**
     * Interface definition for a callback to be invoked when the crop overlay is released.
     */
    public interface OnSetCropOverlayReleasedListener {

        /**
         * Called when the crop overlay changed listener is called and inProgress is false.
         *
         * @param rect The rect coordinates of the cropped overlay
         */
        void onCropOverlayReleased(Rect rect);
    }

    /**
     * Interface definition for a callback to be invoked when the crop overlay is released.
     */
    public interface OnSetCropOverlayMovedListener {

        /**
         * Called when the crop overlay is moved
         *
         * @param rect The rect coordinates of the cropped overlay
         */
        void onCropOverlayMoved(Rect rect);
    }

    /**
     * Interface definition for a callback to be invoked when the crop overlay is released.
     */
    public interface OnSetCropWindowChangeListener {

        /**
         * Called when the crop window is changed
         */
        void onCropWindowChanged();
    }

    /**
     * Interface definition for a callback to be invoked when image async loading is complete.
     */
    public interface OnSetImageUriCompleteListener {

        /**
         * Called when a crop image view has completed loading image for cropping.<br>
         * If loading failed error parameter will contain the error.
         *
         * @param view  The crop image view that loading of image was complete.
         * @param uri   the URI of the image that was loading
         * @param error if error occurred during loading will contain the error, otherwise null.
         */
        void onSetImageUriComplete(CropImageView view, Uri uri, Exception error);
    }
    // endregion

    // region: Inner class: OnGetCroppedImageCompleteListener

    /**
     * Interface definition for a callback to be invoked when image async crop is complete.
     */
    public interface OnCropImageCompleteListener {

        /**
         * Called when a crop image view has completed cropping image.<br>
         * Result object contains the cropped bitmap, saved cropped image uri, crop points data or the
         * error occured during cropping.
         *
         * @param view   The crop image view that cropping of image was complete.
         * @param result the crop image result data (with cropped image or error)
         */
        void onCropImageComplete(CropImageView view, CropResult result);
    }
    // endregion

    // region: Inner class: ActivityResult

    /**
     * Result data of crop image.
     */
    public static class CropResult {

        /**
         * The image bitmap of the original image loaded for cropping.<br>
         * Null if uri used to load image or activity result is used.
         */
        private final Bitmap mOriginalBitmap;

        /**
         * The Android uri of the original image loaded for cropping.<br>
         * Null if bitmap was used to load image.
         */
        private final Uri mOriginalUri;

        /**
         * The cropped image bitmap result.<br>
         * Null if save cropped image was executed, no output requested or failure.
         */
        private final Bitmap mBitmap;

        /**
         * The Android uri of the saved cropped image result.<br>
         * Null if get cropped image was executed, no output requested or failure.
         */
        private final Uri mUri;

        /**
         * The error that failed the loading/cropping (null if successful)
         */
        private final Exception mError;

        /**
         * The 4 points of the cropping window in the source image
         */
        private final float[] mCropPoints;

        /**
         * The rectangle of the cropping window in the source image
         */
        private final Rect mCropRect;

        /**
         * The rectangle of the source image dimensions
         */
        private final Rect mWholeImageRect;

        /**
         * The final rotation of the cropped image relative to source
         */
        private final int mRotation;

        /**
         * sample size used creating the crop bitmap to lower its size
         */
        private final int mSampleSize;

        CropResult(
                Bitmap originalBitmap,
                Uri originalUri,
                Bitmap bitmap,
                Uri uri,
                Exception error,
                float[] cropPoints,
                Rect cropRect,
                Rect wholeImageRect,
                int rotation,
                int sampleSize) {
            mOriginalBitmap = originalBitmap;
            mOriginalUri = originalUri;
            mBitmap = bitmap;
            mUri = uri;
            mError = error;
            mCropPoints = cropPoints;
            mCropRect = cropRect;
            mWholeImageRect = wholeImageRect;
            mRotation = rotation;
            mSampleSize = sampleSize;
        }

        /**
         * The image bitmap of the original image loaded for cropping.<br>
         * Null if uri used to load image or activity result is used.
         */
        public Bitmap getOriginalBitmap() {
            return mOriginalBitmap;
        }

        /**
         * The Android uri of the original image loaded for cropping.<br>
         * Null if bitmap was used to load image.
         */
        public Uri getOriginalUri() {
            return mOriginalUri;
        }

        /**
         * Is the result is success or error.
         */
        public boolean isSuccessful() {
            return mError == null;
        }

        /**
         * The cropped image bitmap result.<br>
         * Null if save cropped image was executed, no output requested or failure.
         */
        public Bitmap getBitmap() {
            return mBitmap;
        }

        /**
         * The Android uri of the saved cropped image result Null if get cropped image was executed, no
         * output requested or failure.
         */
        public Uri getUri() {
            return mUri;
        }

        /**
         * The error that failed the loading/cropping (null if successful)
         */
        public Exception getError() {
            return mError;
        }

        /**
         * The 4 points of the cropping window in the source image
         */
        public float[] getCropPoints() {
            return mCropPoints;
        }

        /**
         * The rectangle of the cropping window in the source image
         */
        public Rect getCropRect() {
            return mCropRect;
        }

        /**
         * The rectangle of the source image dimensions
         */
        public Rect getWholeImageRect() {
            return mWholeImageRect;
        }

        /**
         * The final rotation of the cropped image relative to source
         */
        public int getRotation() {
            return mRotation;
        }

        /**
         * sample size used creating the crop bitmap to lower its size
         */
        public int getSampleSize() {
            return mSampleSize;
        }
    }
    // endregion
}
